/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package chatserver;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author NAMQUYUIT
 */
public class ChatServer {

    public static final int PORT_ACCOUNT = 2114;
    public static final int BUFFER = 1024 * 8;
    public static final String MESSAGE_NAME_REGISTRY = "NAME_REGISTRY";
    public static final String MESSAGE_NAME_REGISTRY_REJECTED = "NAME_REGISTRY_REJECTED";
    public static final String MESSAGE_NAME_REGISTRY_ACCEPTED = "NAME_REGISTRY_ACCEPTED";
    public static final String MESSAGE_ACCOUNT_LOGIN = "ACCOUNT_LOGIN";
    public static final String MESSAGE_ACCOUNT_LOGIN_FAILED = "ACCOUNT_LOGIN_FAILED";
    public static final String MESSAGE_ACCOUNT_LOGIN_SUCCESSED = "ACCOUNT_LOGIN_SUCCESSED";
    public static final String MESSAGE_ACCOUNT_HAS_NEW_USER_LOGIN = "ACCOUNT_HAS_NEW_USER_LOGIN";
    public static final String MESSAGE_FILE_SEND_ASK = "FILE_SEND_ASK";
    public static final String MESSAGE_FILE_SEND_ACCEPT = "FILE_SEND_ACCEPT";
    public static final String MESSAGE_FILE_SEND = "FILE_SEND";
    //public static final String MESSAGE_TO_SERVER = "CLIENT_SERVER_MESSAGE";
    public static final String MESSAGE_TO_CLIENT = "CLIENT_CLIENT_MESSAGE";
    public static final String MESSAGE_CREATE_GROUP = "CREATE_GROUP";
    public static final String MESSAGE_CREATE_GROUP_SUCCESS = "CREATE_GROUP_SUCCESS";
    public static final String MESSAGE_GROUP_ADD_USER = "GROUP_ADD_USER";
    public static final String MESSAGE_GROUP_LEAVE = "GROUP_LEAVE";
    public static final String MESSAGE_GROUP_REMOVE = "GROUP_REMOVE";
    public static final String MESSAGE_GROUP_SEND_MESSAGE = "GROUP_SEND_MESSAGE";
    public static final String MESSAGE_GROUP_FILE_SEND_ASK = "FILE_GROUP_SEND_ASK";
    public static final String MESSAGE_GROUP_FILE_SEND_ACCEPT = "FILE_GROUP_SEND_ACCEPT";
    public static final String MESSAGE_GROUP_FILE_SEND = "FILE_GROUP_SEND";
    
    ServerSocket serverSocket;
    HashMap<String, ChatAccount> listAccounts;
    ArrayList<ArrayList<String>> listGroups;
    

    public ChatServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
        this.listAccounts = new HashMap<>();
        this.listGroups = new ArrayList<>();
    }
    
    public ServerSocket getServerSocket() {
        return this.serverSocket;
    }

    public void addAccount(ChatAccount account) {
        if (this.listAccounts.containsKey(account.getAccountName()) == false) {
            this.listAccounts.put(account.getAccount().getUsername(), account);    
        } else {
            ChatAccount tmpChatAccount = this.listAccounts.get(account.getAccountName());
            account.setListUnreadMessages(tmpChatAccount.getListUnreadMessages());
            this.listAccounts.remove(tmpChatAccount);
            this.listAccounts.put(account.getAccountName(), account);
        }
    }

    public class ChatAccount extends Thread {

        private Account account;
        private Boolean isLogin;
        private String name;
        private Socket socket;
        private BufferedReader input;
        private PrintWriter output;
        private ArrayList<String> listUnreadMessages;

        /**
         * Initialize a client chat with socket connect to server
         */
        public ChatAccount(Socket socket) {
            this.socket = socket;
            this.isLogin = false;
            this.listUnreadMessages = new ArrayList<>();
            try {
                this.input = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
                this.output = new PrintWriter(this.socket.getOutputStream(), true);
            } catch (IOException ex) {
                // do nothing
            }
        }
        
        public String getAccountName() {
            return this.name;
        }

        public Account getAccount() {
            return account;
        }
        
        private void setAccount(String username, String password) {
            this.account = new Account(username, password);
        }

        public Boolean isLogin() {
            return isLogin;
        }

        public void setLogin(Boolean isLogin) {
            this.isLogin = isLogin;
        }

        public ArrayList<String> getListUnreadMessages() {
            return listUnreadMessages;
        }
        
        public void setListUnreadMessages(ArrayList<String> listUnreadMessages) {
            this.listUnreadMessages.addAll(listUnreadMessages);
        }

        public void addUnreadMessage(String message) {
            this.listUnreadMessages.add(message);
        }
        
        public void emptyUnreadInbox() {
            this.listUnreadMessages.clear();
        }

        public Socket getSocket() {
            return socket;
        }
        
        public PrintWriter getPrintWriter() {
            return this.output;
        }
        
        public BufferedReader getInput() {
            return this.input;
        }
        
        public boolean checkLogin(String username, String password) {
            Account tmpAccount = new Account(username, password);
            for (Map.Entry<String, ChatAccount> entry : listAccounts.entrySet()) {
                //String string = entry.getKey();
                ChatAccount chatAccount = entry.getValue();
                if (chatAccount.getAccount().equals(tmpAccount)) {
                    return true;
                }
            }
            
            return false;
        }

        public boolean checkUsernameExisted(String username) {
            return listAccounts.containsKey(username);
        }
        
        /*
         * Send a message to current client
        */
        public boolean sendMessage(String type, String message) {
            if (this.output != null) {
                this.output.println(type + "::" + message);
                return true;
            }
            return false;
        }
        
        /*
         * Send a message to other client with "name"
        */
        public boolean transferMessageTo(String name, String message) {
            if (this.output != null) {
                if (listAccounts.containsKey(name)) {
                    if (listAccounts.get(name).isLogin() == false) {
                        listAccounts.get(name).addUnreadMessage(this.name + "::" + message);
                    } else {
                        listAccounts.get(name).getPrintWriter().println(MESSAGE_TO_CLIENT + "::" + this.name + "::" + message);
                    }
                    return true;
                }
            }
            return false;
        }
        
        /*
         * Send a message to other clients
        */
        public boolean transferMessageToOthers(String type, String message) {
            if (this.output != null) {
                for (Map.Entry<String, ChatAccount> entry : listAccounts.entrySet()) {
                    String name = entry.getKey();
                    if (!this.name.equals(name)) {
                        ChatAccount chatAccount = entry.getValue();
                        chatAccount.getPrintWriter().println(type + "::" + message);
                    }
                }
                return true;
            }
            return false;
        }
        
        public boolean transferMessageToGroup(String type, String groupId, String sender, String message, boolean isIncludeSender) {
            if (this.output != null) {
                try {
                    int tmpGroupId = Integer.valueOf(groupId);
                    ArrayList<String> tmpGroupUsers = listGroups.get(tmpGroupId);
                    if (!isIncludeSender) {
                        for (String username : tmpGroupUsers) {
                            if (listAccounts.containsKey(username) && !username.equals(sender)) {
                                ChatAccount chatAccount = listAccounts.get(username);
                                chatAccount.getPrintWriter().println(type + "::" + groupId + "::" + sender + "::" + message);
                            }
                        }
                    } else {
                        for (String username : tmpGroupUsers) {
                            if (listAccounts.containsKey(username)) {
                                ChatAccount chatAccount = listAccounts.get(username);
                                chatAccount.getPrintWriter().println(type + "::" + groupId + "::" + sender + "::" + message);
                            }
                        }
                    }
                    return true;
                } catch (Exception ex) {
                    
                }
            }
            return false;
        }
        
        /*
         *  Send a file to client
         */
        /*
        public boolean transferFileTo(String name, byte[] byteArray, int bytesRead, String fileName) {
            if (listAccounts.containsKey(name)) {
                try {
                    OutputStream os = listAccounts.get(name).getSocket().getOutputStream();
                    os.write(byteArray, 0, bytesRead);
                    os.flush();
                } catch (IOException ex) {
                    return false;
                }
                return true;
            }
            return false;
        }*/

        public boolean sendListUsers() {
            if (this.output != null) {
                StringBuilder builder = new StringBuilder();
                String tmpStr;
                for (Map.Entry<String, ChatAccount> entry : listAccounts.entrySet()) {
                    tmpStr = entry.getKey();
                    if (!this.name.equals(tmpStr)) {
                        builder.append(tmpStr);
                        builder.append("::");
                    }
                }
                if (builder.length() > 0) {
                    //builder.deleteCharAt(builder.length() - 1);
                    builder.delete(builder.length()-2, builder.length());
                }
                this.output.println(builder.toString());
                System.out.println(builder.toString());
                return true;
            }
            return false;
        }
        
        public boolean sendListUnreadMessages() {
            if (this.output != null) {
                StringBuilder builder = new StringBuilder();
                for (String message : this.listUnreadMessages) {
                    builder.append(message);
                    builder.append("::");
                }
                if (builder.length() > 0) {
                    //builder.deleteCharAt(builder.length() - 1);
                    builder.delete(builder.length()-2, builder.length());
                }
                this.output.println(builder.toString());
                System.out.println(builder.toString());
                
                this.emptyUnreadInbox();
                return true;
            }
            return false;
        }
        
        @Override
        public void run() {
            String command;

            try {
                // set name
                while (true) {
                    command = this.input.readLine();
                    synchronized (listAccounts) {
                        if (command.contains(MESSAGE_NAME_REGISTRY)) { // registry
                            String tmpName = command.split("::")[1];
                            String tmpPassword = command.split("::")[2];
                            if (/*tmpName != null && tmpName.length() > 0 && tmpPassword.length() > 0 && */!listAccounts.containsKey(tmpName)) {
                                this.name = tmpName;
                                this.setAccount(tmpName, tmpPassword);
                                this.setLogin(true);
                                addAccount(this);
                                this.sendMessage(MESSAGE_NAME_REGISTRY_ACCEPTED, name);
                                this.sendListUsers();
                                this.sendListUnreadMessages();
                                this.transferMessageToOthers(MESSAGE_ACCOUNT_HAS_NEW_USER_LOGIN, this.name);
                                System.out.println(MESSAGE_NAME_REGISTRY_ACCEPTED + "::" + this.name);
                                break;
                            } else {
                                this.sendMessage(MESSAGE_NAME_REGISTRY_REJECTED, tmpName);
                                System.out.println(MESSAGE_NAME_REGISTRY_REJECTED + "::" + tmpName);
                            }
                        } else if (command.contains(MESSAGE_ACCOUNT_LOGIN)) { // login
                            String tmpName = command.split("::")[1];
                            String tmpPassword = command.split("::")[2];
                            if (listAccounts.containsKey(tmpName)
                                    && listAccounts.get(tmpName).getAccount().getPassword().equals(tmpPassword)) {
                                this.name = tmpName;
                                this.setAccount(tmpName, tmpPassword);
                                addAccount(this);
                                this.setLogin(true);
                                this.sendMessage(MESSAGE_ACCOUNT_LOGIN_SUCCESSED, tmpName);
                                this.sendListUsers();
                                this.sendListUnreadMessages();
                                System.out.println(MESSAGE_ACCOUNT_LOGIN_SUCCESSED + "::" + this.name);
                                break;
                            } else {
                                this.sendMessage(MESSAGE_ACCOUNT_LOGIN_FAILED, tmpName);
                                System.out.println(MESSAGE_ACCOUNT_LOGIN_FAILED + "::" + tmpName);
                            }
                        }
                    }
                }

                // transfer information
                while (true) {
                    command = this.input.readLine();
                    if (command != null && command.length() > 0) {
                        String[] tmpArr = command.split("::");
                        if (command.contains(MESSAGE_TO_CLIENT)) {
                            this.transferMessageTo(tmpArr[1], tmpArr[2]);
                        } else if (command.contains(MESSAGE_FILE_SEND_ASK)) {
                            if (listAccounts.containsKey(tmpArr[1])) {
                                listAccounts.get(tmpArr[1]).getPrintWriter().println(MESSAGE_FILE_SEND_ASK + "::" + this.name + "::" + tmpArr[2]);
                            }
                        } else if (command.contains(MESSAGE_FILE_SEND_ACCEPT)) {
                            if (listAccounts.containsKey(tmpArr[1])) {
                                listAccounts.get(tmpArr[1]).getPrintWriter().println(MESSAGE_FILE_SEND_ACCEPT + "::" + this.name + "::" + tmpArr[2] + "::" + tmpArr[3]);
                            }
                        } else if (command.contains(MESSAGE_FILE_SEND)) {
                            if (listAccounts.containsKey(tmpArr[1])) {
                                listAccounts.get(tmpArr[1]).getPrintWriter().println(MESSAGE_FILE_SEND + "::" + this.name + "::" + tmpArr[2] + "::" + tmpArr[3] + "::" + tmpArr[4]);

                                byte[] byteArray = new byte[BUFFER];
                                InputStream is = this.socket.getInputStream();
                                OutputStream os = listAccounts.get(tmpArr[1]).getSocket().getOutputStream();
                                int length = Integer.valueOf(tmpArr[3]);
                                int count = 0;
                                int bytesRead;
                                
                                while (count < length) {
                                    bytesRead = is.read(byteArray, 0, byteArray.length);
                                    if (bytesRead > 0) {
                                        count += bytesRead;
                                        os.write(byteArray, 0, bytesRead);
                                        os.flush();
                                    }
                                }
                            }
                        } else if (command.contains(MESSAGE_CREATE_GROUP)) {
                            synchronized(listGroups) {
                                command = this.input.readLine();
                                ArrayList<String> tmpUsers = new ArrayList<>();
                                
                                if (command != null && command.length() > 0) {
                                    tmpArr = command.split("::");
                                    for (String string : tmpArr) {
                                        tmpUsers.add(string);
                                    }
                                    tmpUsers.add(this.name);
                                }
                                listGroups.add(tmpUsers);
                                
                                //this.output.println(MESSAGE_CREATE_GROUP_SUCCESS + "::" + (listGroups.size()-1) + "::" + listGroups);
                                command += "::" + this.name;
                                this.transferMessageToGroup(MESSAGE_CREATE_GROUP_SUCCESS, String.valueOf(listGroups.size()-1), this.name, command, true);
                            }
                        } else if (command.contains(MESSAGE_GROUP_ADD_USER)) {
                            synchronized(listGroups) {
                                try {
                                    int groupId = Integer.valueOf(tmpArr[1]);
                                    if (!listGroups.get(groupId).contains(tmpArr[3])) {
                                        
                                        StringBuilder builder = new StringBuilder();
                                        builder.append(tmpArr[3]);
                                        builder.append("::");
                                        for (String string : listGroups.get(groupId)) {
                                            builder.append(string);
                                            builder.append("::");
                                        }
                                        builder.delete(builder.length()-2, builder.length());
                                        
                                        listGroups.get(groupId).add(tmpArr[3]); // IMPORTANT: before send message
                                        this.transferMessageToGroup(tmpArr[0], tmpArr[1], tmpArr[2], builder.toString(), false);
                                    }
                                } catch (Exception ex) {
                                    
                                }
                            }
                        } else if (command.contains(MESSAGE_GROUP_SEND_MESSAGE)) {
                            synchronized(listGroups) {
                                this.transferMessageToGroup(tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], false);
                            }
                        } else if (command.contains(MESSAGE_GROUP_LEAVE)) {
                            synchronized(listGroups) {
                                try {
                                    int groupId = Integer.valueOf(tmpArr[1]);
                                    if (listGroups.get(groupId).contains(this.name)) {
                                        this.transferMessageToGroup(tmpArr[0], tmpArr[1], this.name, "", false);
                                        listGroups.get(groupId).remove(this.name); // IMPORTANT: after send message
                                    }
                                } catch (Exception ex) {
                                    
                                }
                            }
                        } else if (command.contains(MESSAGE_GROUP_REMOVE)) {
                            synchronized(listGroups) {
                                try {
                                    int groupId = Integer.valueOf(tmpArr[1]);
                                    if (listGroups.get(groupId).contains(tmpArr[2])) {
                                        this.transferMessageToGroup(tmpArr[0], tmpArr[1], this.name, tmpArr[2], false);
                                        listGroups.get(groupId).remove(tmpArr[2]); // IMPORTANT: after send message
                                    }
                                } catch (Exception ex) {
                                    
                                }
                            }
                        } else if (command.contains(MESSAGE_GROUP_FILE_SEND_ASK)) {
                            synchronized(listGroups) {
                                try {
                                    int groupId = Integer.valueOf(tmpArr[1]);
                                    ArrayList<String> tmpListUsers = listGroups.get(groupId);
                                    for (String name : tmpListUsers) {
                                        if (this.name.equals(name) == false) {
                                            listAccounts.get(name).getPrintWriter().println(MESSAGE_FILE_SEND_ASK + "::" + this.name + "::" + tmpArr[2]);
                                        }
                                    }
                                } catch (Exception ex) {
                                    
                                }
                            }
                        }

                        System.out.print(command);
                    }
                }
            } catch (IOException e) {
                // do nothing
            } finally {
                
                if (listAccounts.containsKey(this.name)) {
                    //listAccounts.remove(this.name);
                    listAccounts.get(this.name).setLogin(false);
                }
                try {
                    this.socket.close();
                } catch (IOException ex) {
                }
            }
        }
    }
}
